// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

namespace System.Data.Entity.Core.Metadata.Edm.Provider
{
    using System.Collections.ObjectModel;
    using System.Data.Entity.Core.Common;
    using System.Data.Entity.Hierarchy;
    using System.Data.Entity.Spatial;
    using System.Data.Entity.Utilities;
    using System.Linq;
    using System.Threading;
    using System.Xml;

    internal class ClrProviderManifest : DbProviderManifest
    {
        private const int s_PrimitiveTypeCount = EdmConstants.NumPrimitiveTypes;
        private ReadOnlyCollection<PrimitiveType> _primitiveTypesArray;
        private ReadOnlyCollection<PrimitiveType> _primitiveTypes;
        private static readonly ClrProviderManifest _instance = new ClrProviderManifest();

        // <summary>
        // A private constructor to prevent other places from instantiating this class
        // </summary>
        private ClrProviderManifest()
        {
        }

        // <summary>
        // Gets the EDM provider manifest singleton instance
        // </summary>
        internal static ClrProviderManifest Instance
        {
            get { return _instance; }
        }

        // <summary>
        // Returns the namespace used by this provider manifest
        // </summary>
        public override string NamespaceName
        {
            get { return EdmConstants.ClrPrimitiveTypeNamespace; }
        }

        // <summary>
        // Returns the primitive type corresponding to the given CLR type
        // </summary>
        // <param name="clrType"> The CLR type for which the PrimitiveType object is retrieved </param>
        // <param name="primitiveType"> The retrieved primitive type </param>
        // <returns> True if a primitive type is returned </returns>
        internal bool TryGetPrimitiveType(Type clrType, out PrimitiveType primitiveType)
        {
            primitiveType = null;
            PrimitiveTypeKind resolvedTypeKind;
            if (TryGetPrimitiveTypeKind(clrType, out resolvedTypeKind))
            {
                InitializePrimitiveTypes();
                primitiveType = _primitiveTypesArray[(int)resolvedTypeKind];
                return true;
            }

            return false;
        }

        // <summary>
        // Returns the <see cref="PrimitiveTypeKind" /> corresponding to the given CLR type
        // </summary>
        // <param name="clrType"> The CLR type for which the PrimitiveTypeKind value should be resolved </param>
        // <param name="resolvedPrimitiveTypeKind"> The PrimitiveTypeKind value to which the CLR type resolves, if any. </param>
        // <returns> True if the CLR type represents a primitive (EDM) type; otherwise false. </returns>
        internal static bool TryGetPrimitiveTypeKind(Type clrType, out PrimitiveTypeKind resolvedPrimitiveTypeKind)
        {
            PrimitiveTypeKind? primitiveTypeKind = null;
            if (!clrType.IsEnum()) // Enums return the TypeCode of their underlying type
            {
                // As an optimization, short-circuit when the provided type has a known type code.
                switch (Type.GetTypeCode(clrType))
                {
                    // PrimitiveTypeKind.Binary = byte[] = TypeCode.Object
                    case TypeCode.Boolean:
                        primitiveTypeKind = PrimitiveTypeKind.Boolean;
                        break;
                    case TypeCode.Byte:
                        primitiveTypeKind = PrimitiveTypeKind.Byte;
                        break;
                    case TypeCode.DateTime:
                        primitiveTypeKind = PrimitiveTypeKind.DateTime;
                        break;
                    // PrimitiveTypeKind.DateTimeOffset = System.DateTimeOffset = TypeCode.Object
                    case TypeCode.Decimal:
                        primitiveTypeKind = PrimitiveTypeKind.Decimal;
                        break;
                    case TypeCode.Double:
                        primitiveTypeKind = PrimitiveTypeKind.Double;
                        break;
                    // PrimitiveTypeKind.Geography = System.Data.Entity.Spatial.DbGeometry (or subtype) = TypeCode.Object
                    // PrimitiveTypeKind.Geometry = System.Data.Entity.Spatial.DbGeometry (or subtype) = TypeCode.Object
                    // PrimitiveTypeKind.Guid = System.Guid = TypeCode.Object
                    case TypeCode.Int16:
                        primitiveTypeKind = PrimitiveTypeKind.Int16;
                        break;
                    case TypeCode.Int32:
                        primitiveTypeKind = PrimitiveTypeKind.Int32;
                        break;
                    case TypeCode.Int64:
                        primitiveTypeKind = PrimitiveTypeKind.Int64;
                        break;
                    case TypeCode.SByte:
                        primitiveTypeKind = PrimitiveTypeKind.SByte;
                        break;
                    case TypeCode.Single:
                        primitiveTypeKind = PrimitiveTypeKind.Single;
                        break;
                    case TypeCode.String:
                        primitiveTypeKind = PrimitiveTypeKind.String;
                        break;
                    // PrimitiveTypeKind.Time = System.TimeSpan = TypeCode.Object
                    case TypeCode.Object:
                        {
                            if (typeof(byte[]) == clrType)
                            {
                                primitiveTypeKind = PrimitiveTypeKind.Binary;
                            }
                            else if (typeof(DateTimeOffset) == clrType)
                            {
                                primitiveTypeKind = PrimitiveTypeKind.DateTimeOffset;
                            }
                            // DbGeography/Geometry are abstract so subtypes must be allowed
                            else if (typeof(DbGeography).IsAssignableFrom(clrType))
                            {
                                primitiveTypeKind = PrimitiveTypeKind.Geography;
                            }
                            else if (typeof(DbGeometry).IsAssignableFrom(clrType))
                            {
                                primitiveTypeKind = PrimitiveTypeKind.Geometry;
                            }
                            else if (typeof(Guid) == clrType)
                            {
                                primitiveTypeKind = PrimitiveTypeKind.Guid;
                            }
                            else if (typeof(HierarchyId) == clrType)
                            {
                                primitiveTypeKind = PrimitiveTypeKind.HierarchyId;
                            }
                            else if (typeof(TimeSpan) == clrType)
                            {
                                primitiveTypeKind = PrimitiveTypeKind.Time;
                            }
                            break;
                        }
                }
            }

            if (primitiveTypeKind.HasValue)
            {
                resolvedPrimitiveTypeKind = primitiveTypeKind.Value;
                return true;
            }
            else
            {
                resolvedPrimitiveTypeKind = default(PrimitiveTypeKind);
                return false;
            }
        }

        // <summary>
        // Returns all the functions in this provider manifest
        // </summary>
        // <returns> A collection of functions </returns>
        public override ReadOnlyCollection<EdmFunction> GetStoreFunctions()
        {
            return Helper.EmptyEdmFunctionReadOnlyCollection;
        }

        // <summary>
        // Returns all the FacetDescriptions for a particular type
        // </summary>
        // <param name="type"> the type to return FacetDescriptions for. </param>
        // <returns> The FacetDescriptions for the type given. </returns>
        public override ReadOnlyCollection<FacetDescription> GetFacetDescriptions(EdmType type)
        {
            if (Helper.IsPrimitiveType(type)
                && (type).DataSpace == DataSpace.OSpace)
            {
                // we don't have our own facets, just defer to the edm primitive type facets
                var basePrimitive = (PrimitiveType)type.BaseType;
                return basePrimitive.ProviderManifest.GetFacetDescriptions(basePrimitive);
            }

            return Helper.EmptyFacetDescriptionEnumerable;
        }

        // <summary>
        // Initializes all the primitive types
        // </summary>
        private void InitializePrimitiveTypes()
        {
            if (_primitiveTypes != null)
            {
                return;
            }

            var primitiveTypes = new PrimitiveType[s_PrimitiveTypeCount];
            primitiveTypes[(int)PrimitiveTypeKind.Binary] = CreatePrimitiveType(typeof(Byte[]), PrimitiveTypeKind.Binary);
            primitiveTypes[(int)PrimitiveTypeKind.Boolean] = CreatePrimitiveType(typeof(Boolean), PrimitiveTypeKind.Boolean);
            primitiveTypes[(int)PrimitiveTypeKind.Byte] = CreatePrimitiveType(typeof(Byte), PrimitiveTypeKind.Byte);
            primitiveTypes[(int)PrimitiveTypeKind.DateTime] = CreatePrimitiveType(typeof(DateTime), PrimitiveTypeKind.DateTime);
            primitiveTypes[(int)PrimitiveTypeKind.Time] = CreatePrimitiveType(typeof(TimeSpan), PrimitiveTypeKind.Time);
            primitiveTypes[(int)PrimitiveTypeKind.DateTimeOffset] = CreatePrimitiveType(
                typeof(DateTimeOffset), PrimitiveTypeKind.DateTimeOffset);
            primitiveTypes[(int)PrimitiveTypeKind.Decimal] = CreatePrimitiveType(typeof(Decimal), PrimitiveTypeKind.Decimal);
            primitiveTypes[(int)PrimitiveTypeKind.Double] = CreatePrimitiveType(typeof(Double), PrimitiveTypeKind.Double);
            primitiveTypes[(int)PrimitiveTypeKind.Geography] = CreatePrimitiveType(typeof(DbGeography), PrimitiveTypeKind.Geography);
            primitiveTypes[(int)PrimitiveTypeKind.Geometry] = CreatePrimitiveType(typeof(DbGeometry), PrimitiveTypeKind.Geometry);
            primitiveTypes[(int)PrimitiveTypeKind.Guid] = CreatePrimitiveType(typeof(Guid), PrimitiveTypeKind.Guid);
            primitiveTypes[(int)PrimitiveTypeKind.HierarchyId] = CreatePrimitiveType(typeof(HierarchyId), PrimitiveTypeKind.HierarchyId);
            primitiveTypes[(int)PrimitiveTypeKind.Int16] = CreatePrimitiveType(typeof(Int16), PrimitiveTypeKind.Int16);
            primitiveTypes[(int)PrimitiveTypeKind.Int32] = CreatePrimitiveType(typeof(Int32), PrimitiveTypeKind.Int32);
            primitiveTypes[(int)PrimitiveTypeKind.Int64] = CreatePrimitiveType(typeof(Int64), PrimitiveTypeKind.Int64);
            primitiveTypes[(int)PrimitiveTypeKind.SByte] = CreatePrimitiveType(typeof(SByte), PrimitiveTypeKind.SByte);
            primitiveTypes[(int)PrimitiveTypeKind.Single] = CreatePrimitiveType(typeof(Single), PrimitiveTypeKind.Single);
            primitiveTypes[(int)PrimitiveTypeKind.String] = CreatePrimitiveType(typeof(String), PrimitiveTypeKind.String);

            var readOnlyTypesArray = new ReadOnlyCollection<PrimitiveType>(primitiveTypes);
            var readOnlyTypes = new ReadOnlyCollection<PrimitiveType>(primitiveTypes.Where(t => t != null).ToList());

            // Set the result to _primitiveTypes at the end
            Interlocked.CompareExchange(ref _primitiveTypesArray, readOnlyTypesArray, null);
            Interlocked.CompareExchange(ref _primitiveTypes, readOnlyTypes, null);
        }

        // <summary>
        // Initialize the primitive type with the given
        // </summary>
        // <param name="clrType"> The CLR type of this type </param>
        // <param name="primitiveTypeKind"> The primitive type kind of the primitive type </param>
        private PrimitiveType CreatePrimitiveType(Type clrType, PrimitiveTypeKind primitiveTypeKind)
        {
            // Figures out the base type
            var baseType = MetadataItem.EdmProviderManifest.GetPrimitiveType(primitiveTypeKind);
            var primitiveType = new PrimitiveType(clrType, baseType, this);
            primitiveType.SetReadOnly();
            return primitiveType;
        }

        public override ReadOnlyCollection<PrimitiveType> GetStoreTypes()
        {
            InitializePrimitiveTypes();
            return _primitiveTypes;
        }

        public override TypeUsage GetEdmType(TypeUsage storeType)
        {
            Check.NotNull(storeType, "storeType");

            throw new NotImplementedException();
        }

        public override TypeUsage GetStoreType(TypeUsage edmType)
        {
            Check.NotNull(edmType, "edmType");

            throw new NotImplementedException();
        }

        // <summary>
        // Providers should override this to return information specific to their provider.
        // This method should never return null.
        // </summary>
        // <param name="informationType"> The name of the information to be retrieved. </param>
        // <returns> An XmlReader at the beginning of the information requested. </returns>
        protected override XmlReader GetDbInformation(string informationType)
        {
            throw new NotImplementedException();
        }
    }
}
